/* eslint-disable operator-linebreak */
/* eslint-disable prefer-const */

const common = require('../prelude/common.js');
const generate = require('escodegen').generate;
const babelParser = require('@babel/parser');
const parse = babelParser.parse;

const ALIAS_AS_RELATIVE = common.ALIAS_AS_RELATIVE;
const ALIAS_AS_RESOLVABLE = common.ALIAS_AS_RESOLVABLE;

function forge (pattern, was) {
  return pattern.replace('{c1}', ', ')
                .replace('{v1}', '"' + was.v1 + '"')
                .replace('{c2}', was.v2 ? ', ' : '')
                .replace('{v2}', was.v2 ? ('"' + was.v2 + '"') : '')
                .replace('{c3}', was.v3 ? ' from ' : '')
                .replace('{v3}', was.v3 ? was.v3 : '');
}

function valid2 (v2) {
  return (v2 === undefined) ||
         (v2 === null) ||
         (v2 === 'must-exclude') ||
         (v2 === 'may-exclude');
}

function visitor_REQUIRE_RESOLVE (n) { // eslint-disable-line camelcase
  const c = n.callee;
  if (!c) return null;
  const ci = (c.object
           && c.object.type === 'Identifier'
           && c.object.name === 'require'
           && c.property
           && c.property.type === 'Identifier'
           && c.property.name === 'resolve');
  if (!ci) return null;
  const f = (n.type === 'CallExpression'
          && n.arguments
          && n.arguments[0]
          && n.arguments[0].type === 'Literal');
  if (!f) return null;
  const m = (n.arguments[1]
          && n.arguments[1].type === 'Literal');
  return { v1: n.arguments[0].value,
           v2: m ? n.arguments[1].value : null };
}

function visitor_REQUIRE (n) { // eslint-disable-line camelcase
  const c = n.callee;
  if (!c) return null;
  const ci = (c.type === 'Identifier'
           && c.name === 'require');
  if (!ci) return null;
  const f = (n.type === 'CallExpression'
          && n.arguments
          && n.arguments[0]
          && n.arguments[0].type === 'Literal');
  if (!f) return null;
  const m = (n.arguments[1]
          && n.arguments[1].type === 'Literal');
  return { v1: n.arguments[0].value,
           v2: m ? n.arguments[1].value : null };
}

function visitor_IMPORT (n) { // eslint-disable-line camelcase
  const ni = (n.type === 'ImportDeclaration');
  if (!ni) return null;
  const s = n.specifiers;
  return { v1: n.source.value,
           v3: reconstructSpecifiers(s) };
}

function visitor_PATH_JOIN (n) { // eslint-disable-line camelcase
  const c = n.callee;
  if (!c) return null;
  const ci = (c.object
           && c.object.type === 'Identifier'
           && c.object.name === 'path'
           && c.property
           && c.property.type === 'Identifier'
           && c.property.name === 'join');
  if (!ci) return null;
  const dn = (n.arguments[0]
           && n.arguments[0].type === 'Identifier'
           && n.arguments[0].name === '__dirname');
  if (!dn) return null;
  const f = (n.type === 'CallExpression'
          && n.arguments
          && n.arguments[1]
          && n.arguments[1].type === 'Literal'
          && n.arguments.length === 2); // TODO concate them
  if (!f) return null;
  return { v1: n.arguments[1].value };
}

module.exports.visitor_SUCCESSFUL = function (node, test) { // eslint-disable-line camelcase
  let mustExclude, mayExclude, was;

  was = visitor_REQUIRE_RESOLVE(node);
  if (was) {
    if (test) return forge('require.resolve({v1}{c2}{v2})', was);
    if (!valid2(was.v2)) return null;
    mustExclude = (was.v2 === 'must-exclude');
    mayExclude = (was.v2 === 'may-exclude');
    return { alias: was.v1,
             aliasType: ALIAS_AS_RESOLVABLE,
             mustExclude: mustExclude,
             mayExclude: mayExclude };
  }

  was = visitor_REQUIRE(node);
  if (was) {
    if (test) return forge('require({v1}{c2}{v2})', was);
    if (!valid2(was.v2)) return null;
    mustExclude = (was.v2 === 'must-exclude');
    mayExclude = (was.v2 === 'may-exclude');
    return { alias: was.v1,
             aliasType: ALIAS_AS_RESOLVABLE,
             mustExclude: mustExclude,
             mayExclude: mayExclude };
  }

  was = visitor_IMPORT(node);
  if (was) {
    if (test) return forge('import {v3}{c3}{v1}', was);
    return { alias: was.v1,
             aliasType: ALIAS_AS_RESOLVABLE };
  }

  was = visitor_PATH_JOIN(node);
  if (was) {
    if (test) return forge('path.join(__dirname{c1}{v1})', was);
    return { alias: was.v1,
             aliasType: ALIAS_AS_RELATIVE,
             mayExclude: false };
  }

  return null;
};

function visitor_NONLITERAL (n) { // eslint-disable-line camelcase
  return (function () {
    const c = n.callee;
    if (!c) return null;
    const ci = (c.object
             && c.object.type === 'Identifier'
             && c.object.name === 'require'
             && c.property
             && c.property.type === 'Identifier'
             && c.property.name === 'resolve');
    if (!ci) return null;
    const f = (n.type === 'CallExpression'
            && n.arguments
            && n.arguments[0]
            && n.arguments[0].type !== 'Literal');
    if (!f) return null;
    const m = n.arguments[1];
    if (!m) return { v1: reconstruct(n.arguments[0]) };
    const q = (n.arguments[1]
            && n.arguments[1].type === 'Literal');
    if (!q) return null;
    return { v1: reconstruct(n.arguments[0]),
             v2: n.arguments[1].value };
  }()) || (function () {
    const c = n.callee;
    if (!c) return null;
    const ci = (c.type === 'Identifier'
             && c.name === 'require');
    if (!ci) return null;
    const f = (n.type === 'CallExpression'
            && n.arguments
            && n.arguments[0]
            && n.arguments[0].type !== 'Literal');
    if (!f) return null;
    const m = n.arguments[1];
    if (!m) return { v1: reconstruct(n.arguments[0]) };
    const q = (n.arguments[1]
            && n.arguments[1].type === 'Literal');
    if (!q) return null;
    return { v1: reconstruct(n.arguments[0]),
             v2: n.arguments[1].value };
  }());
}

module.exports.visitor_NONLITERAL = function (node) { // eslint-disable-line camelcase
  let mustExclude, mayExclude, was;

  was = visitor_NONLITERAL(node);
  if (was) {
    if (!valid2(was.v2)) return null;
    mustExclude = (was.v2 === 'must-exclude');
    mayExclude = (was.v2 === 'may-exclude');
    return { alias: was.v1,
             mustExclude: mustExclude,
             mayExclude: mayExclude };
  }

  return null;
};

function visitor_MALFORMED (n) { // eslint-disable-line camelcase
  return (function () {
    const c = n.callee;
    if (!c) return null;
    const ci = (c.object
             && c.object.type === 'Identifier'
             && c.object.name === 'require'
             && c.property
             && c.property.type === 'Identifier'
             && c.property.name === 'resolve');
    if (!ci) return null;
    const f = (n.type === 'CallExpression'
            && n.arguments
            && n.arguments[0]);
    if (!f) return null;
    return { v1: reconstruct(n.arguments[0]) };
  }()) || (function () {
    const c = n.callee;
    if (!c) return null;
    const ci = (c.type === 'Identifier'
             && c.name === 'require');
    if (!ci) return null;
    const f = (n.type === 'CallExpression'
            && n.arguments
            && n.arguments[0]);
    if (!f) return null;
    return { v1: reconstruct(n.arguments[0]) };
  }());
}

module.exports.visitor_MALFORMED = function (node) { // eslint-disable-line camelcase
  let was;

  was = visitor_MALFORMED(node);
  if (was) return { alias: was.v1 };

  return null;
};

function visitor_USESCWD (n) { // eslint-disable-line camelcase
  const c = n.callee;
  if (!c) return null;
  const ci = (c.object
           && c.object.type === 'Identifier'
           && c.object.name === 'path'
           && c.property
           && c.property.type === 'Identifier'
           && c.property.name === 'resolve');
  if (!ci) return null;
  return { v1: n.arguments.map(reconstruct).join(', ') };
}

module.exports.visitor_USESCWD = function (node) { // eslint-disable-line camelcase
  let was;

  was = visitor_USESCWD(node);
  if (was) return { alias: was.v1 };

  return null;
};

function reconstructSpecifiers (specs) {
  if (!specs || !specs.length) return '';
  const defaults = [];
  for (const spec of specs) {
    if (spec.type === 'ImportDefaultSpecifier') {
      defaults.push(spec.local.name);
    }
  }
  const nonDefaults = [];
  for (const spec of specs) {
    if (spec.type === 'ImportSpecifier') {
      if (spec.local.name === spec.imported.name) {
        nonDefaults.push(spec.local.name);
      } else {
        nonDefaults.push(
          spec.imported.name + ' as ' + spec.local.name
        );
      }
    }
  }
  if (nonDefaults.length) {
    defaults.push('{ ' + nonDefaults.join(', ') + ' }');
  }
  return defaults.join(', ');
}

function reconstruct (node) {
  let v = generate(node).replace(/\n/g, '');
  let v2;
  while (true) {
    v2 = v.replace(/\[ /g, '[')
      .replace(/ \]/g, ']')
      .replace(/ {2}/g, ' ');
    if (v2 === v) break;
    v = v2;
  }
  return v2;
}

function traverse (ast, visitor) {
  // modified esprima-walk to support
  // visitor return value and "trying" flag
  let stack = [ [ ast, false ] ];
  let i, j, key;
  let len, item, node, trying, child;
  for (i = 0; i < stack.length; i += 1) {
    item = stack[i];
    node = item[0];
    if (node) {
      trying = item[1] || (node.type === 'TryStatement');
      if (visitor(node, trying)) {
        for (key in node) {
          child = node[key];
          if (child instanceof Array) {
            len = child.length;
            for (j = 0; j < len; j += 1) {
              stack.push([ child[j], trying ]);
            }
          } else
          if (child && typeof child.type === 'string') {
            stack.push([ child, trying ]);
          }
        }
      }
    }
  }
}

module.exports.parse = function (body) {
  return parse(body, {
    allowImportExportEverywhere: true,
    allowReturnOutsideFunction: true,
    ecmaVersion: 8,
    plugins: [ 'estree', 'bigInt', 'classPrivateProperties', 'classProperties' ]
  });
};

module.exports.detect = function (body, visitor) {
  const json = module.exports.parse(body);
  if (!json) return;
  traverse(json, visitor);
};
